package com.czbspringmvc.servlet;

import com.czbspringmvc.annotation.Controller;
import com.czbspringmvc.annotation.RequestMapping;
import com.czbspringmvc.annotation.RequestParam;
import com.czbspringmvc.annotation.ResponseBody;
import com.czbspringmvc.context.CzbWebApplicationContext;
import com.czbspringmvc.handler.CzbHandler;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author 陈正彬
 * 前端控制器
 */
public class CzbDispatcherServlet extends HttpServlet {

    //定义属性handlerList，保存CzbHandler[url和控制器方法的映射]
    private List<CzbHandler> handlerList = new ArrayList<>();

    //定义属性CzbWebApplicationContext，自己的spring容器
    CzbWebApplicationContext czbWebApplicationContext = null;

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        //获取web.xml的contextConfigLocation
        String configLocation = servletConfig.getInitParameter("contextConfigLocation");

        //创建自己的spring容器
        czbWebApplicationContext = new CzbWebApplicationContext(configLocation);
        czbWebApplicationContext.init();

        //调用 initHandlerMapping,完成url和控制器方法的映射
        initHandlerMapping();
        System.out.println("handlerList = " + handlerList);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        System.out.println("CzbDispatcherServlet--doPost测试连接");
        //调用方法完成分发请求
        executeDispatcher(req, resp);
    }

    //编写方法完成url和控制器方法的映射
    private void initHandlerMapping() {
        if (czbWebApplicationContext.ioc.isEmpty()) {
            return;//如果IOC容器为空，直接返回
        }
        for (Map.Entry<String, Object> entry : czbWebApplicationContext.ioc.entrySet()) {
            //取出注入的Object的clazz对象
            Class<?> clazz = entry.getValue().getClass();
            if (clazz.isAnnotationPresent(Controller.class)) {
                //取出ta所有的方法
                Method[] declaredMethods = clazz.getDeclaredMethods();
                //遍历所有方法
                for (Method method : declaredMethods) {
                    //判断方法上是否有RequestMapping注解
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        //取出@RequestMapping的值，就是映射路径
                        RequestMapping requestMappingAnnotation =
                                method.getAnnotation(RequestMapping.class);

                        //getServletContext().getContextPath() 工程路径进行拼接
                        //或者把tomcat配置成 /
                        String url =  requestMappingAnnotation.value();
                        CzbHandler czbHandler = new CzbHandler(url, entry.getValue(), method);
                        handlerList.add(czbHandler);
                    }
                }
            }
        }
    }

    //编写方法通过request对象，返回CzbHandler对象
    private CzbHandler getCzbHandler(HttpServletRequest request) {
        //获取用户请求的uri
        String requestURI = request.getRequestURI();
        for (CzbHandler czbHandler : handlerList) {
            if (requestURI.equals(czbHandler.getUrl())) {//匹配成功
                return czbHandler;
            }
        }
        return null;
    }

    //编写方法完成分发请求
    private void executeDispatcher(HttpServletRequest request,HttpServletResponse response) {
        CzbHandler czbHandler = getCzbHandler(request);
        try {
            if (null == czbHandler) {//用户请求路径不存在
                response.getWriter().print("<h1>404 NOT FOUND</h1>");
            } else {
                //czbHandler.getMethod().invoke(czbHandler.getController(),request,response);
                //优化👆:将需要传递给目标方法的实参，封装到参数数组，然后以反射调用的方式传递给目标方法
                //将HttpServletRequest和HttpServletResponse封装到参数数组
                //1.得到目标方法的形参参数信息
                Class<?>[] parameterTypes = czbHandler.getMethod().getParameterTypes();
                //2.创建一个参数数组（实参数组），后面反射调用目标方法时使用到
                Object[] params = new Object[parameterTypes.length];
                //遍历parameterTypes形参数组，根据形参数组信息，将形参填充到实参数组
                for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> parameterType = parameterTypes[i];
                    //如果形参是HttpServletRequest，将request填充到params
                    //在原生的SpringMVC中是按照类型来匹配，这里简化了使用名字
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                        params[i] = request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i] = response;
                    }
                }
                //将http请求参数封装到params数组，注意填充实参时候的顺序问题
                //1.获取http请求的参数集合
                request.setCharacterEncoding("utf-8");
                Map<String, String[]> parameterMap = request.getParameterMap();
                //2.遍历parameterMap，将请求参数按照顺序填充到实参数组params
                for(Map.Entry<String,String[]> entry : parameterMap.entrySet()) {
                    //取出key
                    String name = entry.getKey();
                    //这里只考虑提交的参数是单值的情况，即不考虑类似checkbox提示的数据
                    String value = entry.getValue()[0];
                    //调用方法
                    int indexRequestParameterIndex =
                            getIndexRequestParameterIndex(czbHandler.getMethod(), name);
                    if (indexRequestParameterIndex != -1) {
                        params[indexRequestParameterIndex] = value;
                    } else {//没有找到@RequestParam对应的参数，就使用默认的机制进行匹配
                        //1.得到目标方法所有的形参的名称(调用方法)
                        //2.对得到目标方法所有的形参名进行遍历，如果匹配就把当前请求的参数值，填充到params
                        List<String> parametersName = getParametersName(czbHandler.getMethod());
                        for (int i = 0; i < parametersName.size(); i++) {
                            //如果请求参数名和目标方法形参名一样，说明匹配成功
                            if (name.equals(parametersName.get(i))) {
                                params[i] = value;
                                break;
                            }
                        }
                    }

                }

                //反射调用目标方法
                Object result = czbHandler.getMethod().invoke(czbHandler.getController(), params);
                //对返回结果进行解析，原生的SpringMVC通过视图解析器来完成
                //这里是直接解析，只把视图解析器的核心机制实现
                if (result instanceof String) {
                    String viewName = (String)result;
                    if (viewName.contains(":")) {
                        //说明返回的String是 forward:/login_ok.jsp或者redirect:/xxx
                        // forward:/login_ok.jsp里的/要进行解析为工程路径，因为我的tomcat配置路径就是一个 /
                        // 所以我没有处理
                        String viewType = viewName.split(":")[0];// forward || redirect ...
                        String viewPage = viewName.split(":")[1];//跳转的页面
                        if ("forward".equals(viewType)) {// 请求转发
                            //如果tomcat工程路径不是一个 / , viewPage要进行拼接
                            request.getRequestDispatcher(viewPage).forward(request,response);
                        } else if ("redirect".equals(viewType)) {//请求重定向
                            response.sendRedirect(viewPage);
                        }
                    } else {
                        //默认情况下是请求转发（没有带":"）
                        request.getRequestDispatcher(viewName).forward(request,response);
                    }
                }//后面还可以拓展
                else if (result instanceof ArrayList) {
                    //判断目标方法是否有@ResponseBody注解
                    Method method = czbHandler.getMethod();
                    if (method.isAnnotationPresent(ResponseBody.class)) {
                        //使用jackson包的工具类，把ArrayList转成json
                        ObjectMapper objectMapper = new ObjectMapper();
                        String resultJson = objectMapper.writeValueAsString(result);
                        //这里简单处理，直接返回
                        response.setContentType("text/html;charset=utf-8");
                        PrintWriter writer = response.getWriter();
                        writer.write(resultJson);
                        writer.flush();
                        writer.close();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //编写方法，返回请求参数是目标方法的第几个形参

    /**
     *
     * @param method 目标方法
     * @param name 请求的参数名
     * @return 返回目标方法的第几个形参
     */
    public int getIndexRequestParameterIndex(Method method,String name) {
        //1.取出method的所有形参
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i< parameters.length; i++) {
            //获得当前形参
            Parameter parameter = parameters[i];
            //判断当前形参是否有@RequestParam注解
            boolean parameterAnnotationPresent = parameter.isAnnotationPresent(RequestParam.class);
            if (parameterAnnotationPresent) {//有@RequestParam
                //取出当前这个参数的@RequestParam(value="xx")
                String value = parameter.getAnnotation(RequestParam.class).value();
                if (name.equals(value)) {
                    return i;//找到请求的参数，对应目标方法的形参的位置
                }
            }
        }
        //如果没有匹配成功
        return -1;
    }

    //编写方法。得到目标方法的所有形参的名称，并放入到集合中返回

    /**
     *
     * @param method 目标方法
     * @return 所有形参的名称，并放入到集合中返回
     */
    public List<String> getParametersName(Method method) {
        List<String> parametersList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        //在默认情况下parameter.getName()得到的名字不是真正的形参名，
        //而是[arg0,arg1,...], 所以用Java8新特性来解决(引入插件)
        for (Parameter parameter : parameters) {
            parametersList.add(parameter.getName());
        }
        System.out.println("parametersList = " + parametersList);
        return parametersList;
    }
}
